﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
namespace ScpControl
{
    class Touchpad
    {
        internal readonly int device; // Xinput controller 0, 1, 2 or 3
        public Touchpad(int controllerID)
        {
            device = controllerID;
        }
        internal const int TOUCHPAD_DATA_OFFSET = 35, PERMISSIBLE_TAP_DRIFT = 32;
        internal readonly int[] TAP_FINGER_DELTA_JITTER_COMPENSATION_DIVISORS = { -1, 16, 8, 4, 2 }; // ignore tick 0, divide by [0] on tick 1 (finger just landed)
        internal int lastTouchPadX, lastTouchPadY,
            lastTouchPadX2, lastTouchPadY2,
            cumulativePotentialTapDeltaX, cumulativePotentialTapDeltaY;
        int maxX, maxY;
        internal bool pressed; // pressing+clicking counteracts the tap behavior
        internal bool lastTouchPadIsDown;
        internal bool lastIsActive, lastIsActive2;
        internal byte lastTouchID;
        internal INPUT[] sendInputs = new INPUT[2]; // will allow for keyboard + mouse/tablet input within one SendInput call, or two mouse events
        internal int ticks = -1; //incremented every input report, in the future use something more reliable
        public void handleTouchpad(byte[] data, bool touchPadIsDown)
        {
            int mouseDeltaX = 0, mouseDeltaY = 0; // just for the first touch
            bool _isActive = (data[0 + TOUCHPAD_DATA_OFFSET] >> 7) != 0 ? false : true; // >= 1 touch detected
            bool _isActive2 = (data[4 + TOUCHPAD_DATA_OFFSET] >> 7) != 0 ? false : true; // > 1 touch detected
            byte touchID = (byte)(data[0 + TOUCHPAD_DATA_OFFSET] & 0x7F);
     
            if (_isActive)
            {
                ticks++;
                int currentX = data[1 + TOUCHPAD_DATA_OFFSET] + ((data[2 + TOUCHPAD_DATA_OFFSET] & 0xF) * 255);
                int currentY = ((data[2 + TOUCHPAD_DATA_OFFSET] & 0xF0) >> 4) + (data[3 + TOUCHPAD_DATA_OFFSET] * 16);
                if (currentX > maxX)
                    maxX = currentX;
                if (currentY > maxY)
                    maxY = currentY;
                //add secondary touch data
                int currentX2 = data[5 + TOUCHPAD_DATA_OFFSET] + ((data[6 + TOUCHPAD_DATA_OFFSET] & 0xF) * 255);
                int currentY2 = ((data[6 + TOUCHPAD_DATA_OFFSET] & 0xF0) >> 4) + (data[7 + TOUCHPAD_DATA_OFFSET] * 16);

                if (lastIsActive)
                {
                    double sensitivity = Global.getTouchSensitivity(device) / 100.0;
                    mouseDeltaX = (int)(sensitivity * (currentX - lastTouchPadX));
                    mouseDeltaY = (int)(sensitivity * (currentY - lastTouchPadY));
                    //prevent jitter of  the cursor
                    /* XXX we need to log events and "rewind" essentially, not just ignore data.
                     * If we want to actually remove jitter directly, we need to evaluate it much sooner than 1/10th of a
                     * second or the drag->lift->drop finger->drag->REPEAT sequence feels awful.  We can use sixaxis data
                     * to implement better jitter control but we need to start by maintaining a journal/log. */
                    if (Global.getTouchpadJitterCompensation(device))
                    {
                        if (ticks < TAP_FINGER_DELTA_JITTER_COMPENSATION_DIVISORS.Length)
                        {
                            int divisor = TAP_FINGER_DELTA_JITTER_COMPENSATION_DIVISORS[ticks];
                            mouseDeltaX /= divisor;
                            mouseDeltaY /= divisor;
                        }
                        if (ticks < Global.getTapSensitivity(device))
                        {
                            cumulativePotentialTapDeltaX += mouseDeltaX;
                            cumulativePotentialTapDeltaY += mouseDeltaY;
                        }
                    }
                    if (touchPadIsDown)
                    {
                        if (!lastTouchPadIsDown)
                        {
                            mouseDeltaX = 0;
                            mouseDeltaY = 0;
                            //own right click (more than 1 touch detected) - if enabled.
                            //if its Southeast quadrant do a right click (resolution and range of touchpad values is roughly 1920x960)
                            if (!Global.getLowerRCOff(device) && currentX > 1920 * 3 / 4 && currentY > 960 * 3 / 4)
                            {
                                performRightClick();
                                pressed = true;
                            }
                            else if (_isActive2)
                            {
                                if (!mapTouchPad(DS4Controls.TouchMulti))
                                {
                                    performRightClick();
                                    pressed = true;
                                }
                            }
                            //perform a mouse down event when touchpad pressed
                            else if (!mapTouchPad(DS4Controls.TouchButton))
                            {
                                MouseEvent(MOUSEEVENTF_LEFTDOWN);
                                pressed = true;
                            }
                        }
                    }
                    //perform a mouse up event when touchpad released
                    else if (lastTouchPadIsDown)
                    {
                        mouseDeltaX = 0;
                        mouseDeltaY = 0;
                        if (lastIsActive2)
                            mapTouchPad(DS4Controls.TouchMulti, true);
                        else if (!mapTouchPad(DS4Controls.TouchButton, true))
                            MouseEvent(MOUSEEVENTF_LEFTUP);
                    }
                    //either zoom or scroll
                    if (_isActive2 && lastIsActive2)
                    {
                        //mouse wheel 120 == 1 wheel click according to Windows API
                        int lastMidX = (lastTouchPadX + lastTouchPadX2) / 2, lastMidY = (lastTouchPadY + lastTouchPadY2) / 2,
                            currentMidX = (currentX + currentX2) / 2, currentMidY = (currentY + currentY2) / 2; // XXX Will controller swap touch IDs?
                        double coefficient = Global.getScrollSensitivity(device);
                        // Adjust for touch distance: "standard" distance is 960 pixels, i.e. half the width.  Scroll farther if fingers are farther apart, and vice versa, in linear proportion.
                        double touchXDistance = currentX2 - currentX, touchYDistance = currentY2 - currentY, touchDistance = Math.Sqrt(touchXDistance * touchXDistance + touchYDistance * touchYDistance);
                        coefficient *= touchDistance / 960.0;
                        MouseWheel((int)(coefficient * (lastMidY - currentMidY)), (int)(coefficient * (currentMidX - lastMidX)));
                        mouseDeltaY = 0;
                        mouseDeltaX = 0;
                    }
                }
                lastTouchPadX = currentX;
                lastTouchPadY = currentY;
                //secondary touch data
                lastTouchPadX2 = currentX2;
                lastTouchPadY2 = currentY2;
                lastTouchPadIsDown = touchPadIsDown;
            }
            else // finger(s) lifted from touchpad while virtual mouse button(s) clicked
            {
                int currentX = data[1 + TOUCHPAD_DATA_OFFSET] + ((data[2 + TOUCHPAD_DATA_OFFSET] & 0xF) * 255);
                int currentY = ((data[2 + TOUCHPAD_DATA_OFFSET] & 0xF0) >> 4) + (data[3 + TOUCHPAD_DATA_OFFSET] * 16);
                // Click trackpad's top edge (no touch movement) - configurable
                if (touchPadIsDown)
                {
                    if (!lastTouchPadIsDown)
                        if (!mapTouchPad(DS4Controls.TouchUpper))
                            performMiddleClick();
                }
                else if (lastTouchPadIsDown)
                        mapTouchPad(DS4Controls.TouchUpper, true);
                // Set new last touchpad down value
                lastTouchPadIsDown = touchPadIsDown;
                if (!lastIsActive) // neither active before or now
                    return;
                else if (!pressed) // don't do a second tap if this was a press of the touchpad switch
                { // left click synthesis upon finger tap lift
                    if (Global.getTouchpadJitterCompensation(device) == false)
                        performLeftClick();
                    else if (ticks < Global.getTapSensitivity(device))
                    {
                        // measuring drift to prevent jitter.  XXX we should review the entire journal for the tap sensitivity period
                        if (cumulativePotentialTapDeltaX * cumulativePotentialTapDeltaX + cumulativePotentialTapDeltaY * cumulativePotentialTapDeltaY < PERMISSIBLE_TAP_DRIFT * PERMISSIBLE_TAP_DRIFT)
                        {
                            performLeftClick(-cumulativePotentialTapDeltaX, -cumulativePotentialTapDeltaY);
                            mouseDeltaX = mouseDeltaY = 0;
                        }
                    }
                }
            }
            if (lastTouchID != touchID)
            {
                lastTouchID = touchID;
                ticks = -1;
                cumulativePotentialTapDeltaX = cumulativePotentialTapDeltaY = 0;
                pressed = false;
            }
            lastIsActive = _isActive;
            lastIsActive2 = _isActive2;
            MoveCursorBy(mouseDeltaX, mouseDeltaY);
        }

        bool mapTouchPad(DS4Controls padControl, bool release = false)
        {
            ushort key = Global.getCustomKey(padControl);
            if (key == 0)
                return false;
            else
            {
                DS4KeyType keyType = Global.getCustomKeyType(padControl);
                if (!release)
                    if (keyType.HasFlag(DS4KeyType.ScanCode))
                        performSCKeyPress(key);
                    else performKeyPress(key);
                else
                    if (!keyType.HasFlag(DS4KeyType.Repeat))
                        if (keyType.HasFlag(DS4KeyType.ScanCode))
                            performSCKeyRelease(key);
                        else performKeyRelease(key);
                return true;
            }
        }

        void MoveCursorBy(int x, int y)
        {
            if (x != 0 || y != 0)
            {
                sendInputs[0].Type = INPUT_MOUSE;
                sendInputs[0].Data.Mouse.ExtraInfo = IntPtr.Zero;
                sendInputs[0].Data.Mouse.Flags = MOUSEEVENTF_MOVE;
                sendInputs[0].Data.Mouse.MouseData = 0;
                sendInputs[0].Data.Mouse.Time = 0;
                sendInputs[0].Data.Mouse.X = x;
                sendInputs[0].Data.Mouse.Y = y;
                uint result = SendInput(1, sendInputs, Marshal.SizeOf(sendInputs[0]));
            }
        }

        public void MouseWheel(int vertical, int horizontal)
        {
            uint inputs = 0;
            if (vertical != 0)
            {
                sendInputs[inputs].Type = INPUT_MOUSE;
                sendInputs[inputs].Data.Mouse.ExtraInfo = IntPtr.Zero;
                sendInputs[inputs].Data.Mouse.Flags = MOUSEEVENTF_WHEEL;
                sendInputs[inputs].Data.Mouse.MouseData = (uint)vertical;
                sendInputs[inputs].Data.Mouse.Time = 0;
                sendInputs[inputs].Data.Mouse.X = 0;
                sendInputs[inputs].Data.Mouse.Y = 0;
                inputs++;
            }
            if (horizontal != 0)
            {
                sendInputs[inputs].Type = INPUT_MOUSE;
                sendInputs[inputs].Data.Mouse.ExtraInfo = IntPtr.Zero;
                sendInputs[inputs].Data.Mouse.Flags = MOUSEEVENTF_HWHEEL;
                sendInputs[inputs].Data.Mouse.MouseData = (uint)horizontal;
                sendInputs[inputs].Data.Mouse.Time = 0;
                sendInputs[inputs].Data.Mouse.X = 0;
                sendInputs[inputs].Data.Mouse.Y = 0;
                inputs++;
            }
            SendInput(inputs, sendInputs, (int)inputs * Marshal.SizeOf(sendInputs[0]));
        }

        public void MouseEvent(uint mouseButton)
        {
            sendInputs[0].Type = INPUT_MOUSE;
            sendInputs[0].Data.Mouse.ExtraInfo = IntPtr.Zero;
            sendInputs[0].Data.Mouse.Flags = mouseButton;
            sendInputs[0].Data.Mouse.MouseData = 0;
            sendInputs[0].Data.Mouse.Time = 0;
            sendInputs[0].Data.Mouse.X = 0;
            sendInputs[0].Data.Mouse.Y = 0;
            uint result = SendInput(1, sendInputs, Marshal.SizeOf(sendInputs[0]));
        }

        public void performLeftClick(int x = 0, int y = 0)
        {
            sendInputs[0].Type = INPUT_MOUSE;
            sendInputs[0].Data.Mouse.ExtraInfo = IntPtr.Zero;
            sendInputs[0].Data.Mouse.Flags = 0;
            sendInputs[0].Data.Mouse.Flags |= MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP;
            if (x != 0 || y != 0)
                sendInputs[0].Data.Mouse.Flags |= MOUSEEVENTF_MOVE;
            sendInputs[0].Data.Mouse.MouseData = 0;
            sendInputs[0].Data.Mouse.Time = 0;
            sendInputs[0].Data.Mouse.X = x;
            sendInputs[0].Data.Mouse.Y = y;
            uint result = SendInput(1, sendInputs, Marshal.SizeOf(sendInputs[0]));
        }

        public void performRightClick()
        {
            sendInputs[0].Type = INPUT_MOUSE;
            sendInputs[0].Data.Mouse.ExtraInfo = IntPtr.Zero;
            sendInputs[0].Data.Mouse.Flags = 0;
            sendInputs[0].Data.Mouse.Flags |= MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP;
            sendInputs[0].Data.Mouse.MouseData = 0;
            sendInputs[0].Data.Mouse.Time = 0;
            sendInputs[0].Data.Mouse.X = 0;
            sendInputs[0].Data.Mouse.Y = 0;
            uint result = SendInput(1, sendInputs, Marshal.SizeOf(sendInputs[0]));
        }

        public void performMiddleClick()
        {
            sendInputs[0].Type = INPUT_MOUSE;
            sendInputs[0].Data.Mouse.ExtraInfo = IntPtr.Zero;
            sendInputs[0].Data.Mouse.Flags = 0;
            sendInputs[0].Data.Mouse.Flags |= MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP;
            sendInputs[0].Data.Mouse.MouseData = 0;
            sendInputs[0].Data.Mouse.Time = 0;
            sendInputs[0].Data.Mouse.X = 0;
            sendInputs[0].Data.Mouse.Y = 0;
            uint result = SendInput(1, sendInputs, Marshal.SizeOf(sendInputs[0]));
        }

        public void performSCKeyPress(ushort key)
        {
            sendInputs[0].Type = INPUT_KEYBOARD;
            sendInputs[0].Data.Keyboard.ExtraInfo = IntPtr.Zero;
            sendInputs[0].Data.Keyboard.Flags = KEYEVENTF_SCANCODE;
            sendInputs[0].Data.Keyboard.Scan = MapVirtualKey(key, MAPVK_VK_TO_VSC); ;
            sendInputs[0].Data.Keyboard.Time = 0;
            sendInputs[0].Data.Keyboard.Vk = key;
            uint result = SendInput(1, sendInputs, Marshal.SizeOf(sendInputs[0]));
        }

        public void performKeyPress(ushort key)
        {
            sendInputs[0].Type = INPUT_KEYBOARD;
            sendInputs[0].Data.Keyboard.ExtraInfo = IntPtr.Zero;
            sendInputs[0].Data.Keyboard.Flags = 0;
            sendInputs[0].Data.Keyboard.Scan = 0;
            sendInputs[0].Data.Keyboard.Time = 0;
            sendInputs[0].Data.Keyboard.Vk = key;
            uint result = SendInput(1, sendInputs, Marshal.SizeOf(sendInputs[0]));
        }

        public void performSCKeyRelease(ushort key)
        {
            sendInputs[0].Type = INPUT_KEYBOARD;
            sendInputs[0].Data.Keyboard.ExtraInfo = IntPtr.Zero;
            sendInputs[0].Data.Keyboard.Flags = KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP;
            sendInputs[0].Data.Keyboard.Scan = MapVirtualKey(key, MAPVK_VK_TO_VSC);
            sendInputs[0].Data.Keyboard.Time = 0;
            sendInputs[0].Data.Keyboard.Vk = key;
            uint result = SendInput(1, sendInputs, Marshal.SizeOf(sendInputs[0]));
        }

        public void performKeyRelease(ushort key)
        {
            sendInputs[0].Type = INPUT_KEYBOARD;
            sendInputs[0].Data.Keyboard.ExtraInfo = IntPtr.Zero;
            sendInputs[0].Data.Keyboard.Flags = KEYEVENTF_KEYUP;
            sendInputs[0].Data.Keyboard.Scan = 0;
            sendInputs[0].Data.Keyboard.Time = 0;
            sendInputs[0].Data.Keyboard.Vk = key;
            uint result = SendInput(1, sendInputs, Marshal.SizeOf(sendInputs[0]));
        }

        /// <summary>
        /// http://msdn.microsoft.com/en-us/library/windows/desktop/ms646270(v=vs.85).aspx
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        internal struct INPUT
        {
            public uint Type;
            public MOUSEKEYBDHARDWAREINPUT Data;
        }
        internal const uint INPUT_MOUSE = 0, INPUT_KEYBOARD = 1, INPUT_HARDWARE = 2;

        /// <summary>
        /// http://social.msdn.microsoft.com/Forums/en/csharplanguage/thread/f0e82d6e-4999-4d22-b3d3-32b25f61fb2a
        /// </summary>
        [StructLayout(LayoutKind.Explicit)]
        internal struct MOUSEKEYBDHARDWAREINPUT
        {
            [FieldOffset(0)]
            public HARDWAREINPUT Hardware;
            [FieldOffset(0)]
            public KEYBDINPUT Keyboard;
            [FieldOffset(0)]
            public MOUSEINPUT Mouse;
        }

        /// <summary>
        /// http://msdn.microsoft.com/en-us/library/windows/desktop/ms646310(v=vs.85).aspx
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        internal struct HARDWAREINPUT
        {
            public uint Msg;
            public ushort ParamL;
            public ushort ParamH;
        }

        /// <summary>
        /// http://msdn.microsoft.com/en-us/library/windows/desktop/ms646310(v=vs.85).aspx
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        internal struct KEYBDINPUT
        {
            public ushort Vk;
            public ushort Scan;
            public uint Flags;
            public uint Time;
            public IntPtr ExtraInfo;
        }

        /// <summary>
        /// http://social.msdn.microsoft.com/forums/en-US/netfxbcl/thread/2abc6be8-c593-4686-93d2-89785232dacd
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        internal struct MOUSEINPUT
        {
            public int X;
            public int Y;
            public uint MouseData;
            public uint Flags;
            public uint Time;
            public IntPtr ExtraInfo;
        }

        internal const uint MOUSEEVENTF_MOVE = 1, // just apply X/Y (delta due to not setting absolute flag)
            MOUSEEVENTF_LEFTDOWN = 2, MOUSEEVENTF_LEFTUP = 4,
            MOUSEEVENTF_RIGHTDOWN = 8, MOUSEEVENTF_RIGHTUP = 16,
            MOUSEEVENTF_MIDDLEDOWN = 32, MOUSEEVENTF_MIDDLEUP = 64,
            KEYEVENTF_KEYUP = 2, MOUSEEVENTF_WHEEL = 0x0800, MOUSEEVENTF_HWHEEL = 0x1000,
            KEYEVENTF_SCANCODE = 0x0008, MAPVK_VK_TO_VSC = 0;

        [DllImport("user32.dll", SetLastError = true)]
        private static extern uint SendInput(uint numberOfInputs, INPUT[] inputs, int sizeOfInputs);
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern ushort MapVirtualKey(uint uCode, uint uMapType);
    }
}
